use std::time::Duration;

use crate::{
    bson::{doc, oid::ObjectId, Bson, Document},
    bson_util,
    cmap::StreamDescription,
    coll::options::ReturnDocument,
    operation::{test::handle_response_test, FindAndModify, Operation},
    options::{
        FindOneAndDeleteOptions,
        FindOneAndReplaceOptions,
        FindOneAndUpdateOptions,
        Hint,
        UpdateModifications,
    },
    Namespace,
};

// delete tests

fn empty_delete() -> FindAndModify {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! {};
    FindAndModify::with_delete(ns, filter, None)
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_delete_hint() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! {
        "x": 2,
        "y": { "$gt": 1 },
    };

    let options = FindOneAndDeleteOptions {
        hint: Some(Hint::Keys(doc! { "x": 1, "y": -1 })),
        ..Default::default()
    };

    let mut op = FindAndModify::<Document>::with_delete(ns, filter.clone(), Some(options));

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "hint": {
            "x": 1,
            "y": -1,
        },
        "remove": true
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_delete_no_options() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };

    let mut op = FindAndModify::<Document>::with_delete(ns, filter.clone(), None);

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "remove": true
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_delete() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let max_time = Duration::from_millis(2u64);
    let options = FindOneAndDeleteOptions {
        max_time: Some(max_time),
        ..Default::default()
    };

    let mut op = FindAndModify::<Document>::with_delete(ns, filter.clone(), Some(options));

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "maxTimeMS": max_time.as_millis() as i32,
        "remove": true
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_success_delete() {
    let op = empty_delete();
    let value = doc! {
        "_id" : Bson::ObjectId(ObjectId::new()),
        "name" : "Tom",
        "state" : "active",
        "rating" : 100,
        "score" : 5
    };
    let ok_response = doc! {
        "lastErrorObject" : {
            "connectionId" : 1,
            "updatedExisting" : true,
            "n" : 1,
            "syncMillis" : 0,
            "writtenTo" : null,
            "err" : null,
            "ok" : 1
         },
        "value" : value.clone(),
        "ok" : 1
    };

    let result = handle_response_test(&op, ok_response).unwrap();
    assert_eq!(result.unwrap(), value);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_null_value_delete() {
    let op = empty_delete();

    let result = handle_response_test(&op, doc! { "ok": 1.0, "value": Bson::Null }).unwrap();
    assert_eq!(result, None);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_no_value_delete() {
    let op = empty_delete();

    handle_response_test(&op, doc! { "ok": 1.0 }).unwrap_err();
}

// replace tests

fn empty_replace() -> FindAndModify {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! {};
    let replacement = doc! { "x": { "inc": 1 } };
    FindAndModify::with_replace(ns, filter, replacement, None).unwrap()
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_replace_hint() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let replacement = doc! { "x": { "inc": 1 } };
    let options = FindOneAndReplaceOptions {
        hint: Some(Hint::Keys(doc! { "x": 1, "y": -1 })),
        upsert: Some(false),
        bypass_document_validation: Some(true),
        return_document: Some(ReturnDocument::After),
        ..Default::default()
    };

    let mut op = FindAndModify::<Document>::with_replace(
        ns,
        filter.clone(),
        replacement.clone(),
        Some(options),
    )
    .unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": replacement,
        "upsert": false,
        "bypassDocumentValidation": true,
        "new": true,
        "hint": {
            "x": 1,
            "y": -1,
        },
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_replace_no_options() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let replacement = doc! { "x": { "inc": 1 } };

    let mut op =
        FindAndModify::<Document>::with_replace(ns, filter.clone(), replacement.clone(), None)
            .unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": replacement,
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_replace() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let replacement = doc! { "x": { "inc": 1 } };
    let options = FindOneAndReplaceOptions {
        upsert: Some(false),
        bypass_document_validation: Some(true),
        return_document: Some(ReturnDocument::After),
        ..Default::default()
    };

    let mut op = FindAndModify::<Document>::with_replace(
        ns,
        filter.clone(),
        replacement.clone(),
        Some(options),
    )
    .unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": replacement,
        "upsert": false,
        "bypassDocumentValidation": true,
        "new": true
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_success_replace() {
    let op = empty_replace();
    let value = doc! {
        "_id" : Bson::ObjectId(ObjectId::new()),
        "name" : "Tom",
        "state" : "active",
        "rating" : 100,
        "score" : 5
    };
    let ok_response = doc! {
        "lastErrorObject" : {
            "connectionId" : 1,
            "updatedExisting" : true,
            "n" : 1,
            "syncMillis" : 0,
            "writtenTo" : null,
            "err" : null,
            "ok" : 1
         },
        "value" : value.clone(),
        "ok" : 1
    };

    let result = handle_response_test(&op, ok_response).unwrap();
    assert_eq!(result.unwrap(), value);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_null_value_replace() {
    let op = empty_replace();
    let result = handle_response_test(&op, doc! { "ok": 1.0, "value": Bson::Null }).unwrap();
    assert_eq!(result, None);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_no_value_replace() {
    let op = empty_replace();
    handle_response_test(&op, doc! { "ok": 1.0 }).unwrap_err();
}

// update tests

fn empty_update() -> FindAndModify {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! {};
    let update = UpdateModifications::Document(doc! { "$x": { "$inc": 1 } });
    FindAndModify::with_update(ns, filter, update, None).unwrap()
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_update_hint() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let update = UpdateModifications::Document(doc! { "$x": { "$inc": 1 } });
    let options = FindOneAndUpdateOptions {
        hint: Some(Hint::Keys(doc! { "x": 1, "y": -1 })),
        upsert: Some(false),
        bypass_document_validation: Some(true),
        ..Default::default()
    };

    let mut op =
        FindAndModify::<Document>::with_update(ns, filter.clone(), update.clone(), Some(options))
            .unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": update.to_bson(),
        "upsert": false,
        "bypassDocumentValidation": true,
        "hint": {
            "x": 1,
            "y": -1,
        },
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_update_no_options() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let update = UpdateModifications::Document(doc! { "$x": { "$inc": 1 } });
    let mut op =
        FindAndModify::<Document>::with_update(ns, filter.clone(), update.clone(), None).unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": update.to_bson(),
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn build_with_update() {
    let ns = Namespace {
        db: "test_db".to_string(),
        coll: "test_coll".to_string(),
    };
    let filter = doc! { "x": { "$gt": 1 } };
    let update = UpdateModifications::Document(doc! { "$x": { "$inc": 1 } });
    let options = FindOneAndUpdateOptions {
        upsert: Some(false),
        bypass_document_validation: Some(true),
        ..Default::default()
    };

    let mut op =
        FindAndModify::<Document>::with_update(ns, filter.clone(), update.clone(), Some(options))
            .unwrap();

    let description = StreamDescription::new_testing();
    let mut cmd = op.build(&description).unwrap();

    assert_eq!(cmd.name.as_str(), "findAndModify");
    assert_eq!(cmd.target_db.as_str(), "test_db");

    let mut expected_body = doc! {
        "findAndModify": "test_coll",
        "query": filter,
        "update": update.to_bson(),
        "upsert": false,
        "bypassDocumentValidation": true
    };

    bson_util::sort_document(&mut cmd.body);
    bson_util::sort_document(&mut expected_body);

    assert_eq!(cmd.body, expected_body);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_success_update() {
    let op = empty_update();
    let value = doc! {
        "_id" : Bson::ObjectId(ObjectId::new()),
        "name" : "Tom",
        "state" : "active",
        "rating" : 100,
        "score" : 5
    };
    let ok_response = doc! {
        "lastErrorObject" : {
            "connectionId" : 1,
            "updatedExisting" : true,
            "n" : 1,
            "syncMillis" : 0,
            "writtenTo" : null,
            "err" : null,
            "ok" : 1
         },
        "value" : value.clone(),
        "ok" : 1
    };

    let result = handle_response_test(&op, ok_response).unwrap();
    assert_eq!(result.unwrap(), value);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_null_value_update() {
    let op = empty_update();
    let result = handle_response_test(&op, doc! { "ok": 1.0, "value": Bson::Null }).unwrap();
    assert_eq!(result, None);
}

#[cfg_attr(feature = "tokio-runtime", tokio::test)]
#[cfg_attr(feature = "async-std-runtime", async_std::test)]
async fn handle_no_value_update() {
    let op = empty_update();
    handle_response_test(&op, doc! { "ok": 1.0 }).unwrap_err();
}
